#include "pureclog.h"
#if (defined _MSC_VER)
#include <Windows.h>
#include <io.h>
#else
#include <sys/time.h>
#include <unistd.h>
#include <pthread.h>
#include <unistd.h>
#include <libgen.h>
#endif
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "ring_queue.h"
#define MAXLOGSIZE 1024
#define MAXPATH 1024
// font color
#define FBLACK "\x1B[30m"
#define FRED "\x1B[31m"
#define FGREEN "\x1B[32m"
#define FYELLOW "\x1B[33m"
#define FBLUE "\x1B[34m"
#define FMAGENTA "\x1B[35m"
#define FCYAN "\x1B[36m"
#define FWHITE "\x1B[37m"
// background color
#define BGBLACK "\x1B[40m"
#define BGRED "\x1B[41m"
#define BGGREEN "\x1B[42m"
#define BGYELLOW "\x1B[43m"
#define BGBLUE "\x1B[44m"
#define BGMAGENTA "\x1B[45m"
#define BGCYAN "\x1B[46m"
#define BGWHITE "\x1B[47m"
// font style
#define HIGHLIGHT "\x1B[1m"
#define UNDERLINE "\x1B[4m"
#define BLINK "\x1B[5m"
#define REVERSE "\x1B[7m"
#define BLANK "\x1B[8m"

#define RESETCOLOR "\x1B[0m"
// #endif

#if (defined _MSC_VER)
#pragma comment(lib, "ring_queue.lib")
static CRITICAL_SECTION s_section;
#else
static pthread_mutex_t s_mutex = PTHREAD_MUTEX_INITIALIZER;
#endif
typedef struct {
  char consolelogstr[MAXLOGSIZE];
  char consolesrcinfo[MAXLOGSIZE];
  char logfile[MAXPATH];
  int maxlogfilesize;
  Ring_Queue *logqueue;
#ifdef _MSC_VER
  HANDLE logthread;
#else
  pthread_t logthread;
#endif
  int willrun;
  LOGLEVEL streamlevel;
  LOGLEVEL filelevel;
  unsigned int logsize;
  FILE *fp;
} LOGFILEINFO;
const char *s_levelstr[] = {
    "DEBUG",
    "INFO",
    "WARN",
    "ERR",
    "CLOSE"};
typedef struct {
  char logstr[MAXLOGSIZE];
  char srcinfo[MAXLOGSIZE];
  LOGLEVEL level;
  struct timeval tv;
} LOGNODE;
void getcolor(LOGLEVEL level, char **ppcolor, char **ppbkcolor, char **ppstyle) {
  switch (level) {
  case PURECLOG_INFO: {
    *ppcolor = FGREEN;
    *ppbkcolor = "";
    *ppstyle = "";
    break;
  }
  case PURECLOG_WARN: {
    *ppcolor = FYELLOW;
    *ppbkcolor = "";
    *ppstyle = "";

    break;
  }
  case PURECLOG_ERR: {
    *ppcolor = FRED;
    *ppbkcolor = "";
    *ppstyle = "";
    break;
  }
  default:
    *ppcolor = FWHITE;
    *ppbkcolor = "";
    *ppstyle = "";
  }
}
static void outputstr_console(LOGFILEINFO *info, struct timeval tv, LOGLEVEL level) { // LOGLEVEL streamlevel,LOGLEVEL filelevel,FILE* fp,LOGNODE* node){
  struct tm tm;
#if (defined _WIN32 || defined _WIN64)
  time_t tmpsec = tv.tv_sec;
  localtime_s(&tm, (time_t *)&tmpsec);
#else
  localtime_r((time_t *)&tv.tv_sec, &tm);
#endif

  if (info->streamlevel <= level && info->streamlevel != PURECLOG_CLOSE) {
    char *color, *bkcolor, *style;
    getcolor(level, &color, &bkcolor, &style);
    printf("[%04d-%02d-%02d %02d:%02d:%02d %ld %s%s%s%s%s %s]:%s%s%s%s%s", tm.tm_year + 1900,
           tm.tm_mon + 1,
           tm.tm_mday,
           tm.tm_hour,
           tm.tm_min,
           tm.tm_sec,
           tv.tv_usec / 1000, color, bkcolor, style,
           s_levelstr[level], RESETCOLOR, info->consolesrcinfo, color, bkcolor, style,
           info->consolelogstr, RESETCOLOR);
  }
}
static void outputstr(LOGFILEINFO *info, LOGNODE *node) { // LOGLEVEL streamlevel,LOGLEVEL filelevel,FILE* fp,LOGNODE* node){
  LOGLEVEL streamlevel = info->streamlevel;
  LOGLEVEL filelevel = info->filelevel;
  FILE *fp = info->fp;

  struct tm tm;
#if (defined _WIN32 || defined _WIN64)
  time_t tmpsec = node->tv.tv_sec;
  localtime_s(&tm, (time_t *)&tmpsec);
#else
  localtime_r((time_t *)&node->tv.tv_sec, &tm);
#endif

  if (filelevel <= node->level && filelevel != PURECLOG_CLOSE) {
    int writelen = fprintf(fp, "[%04d-%02d-%02d %02d:%02d:%02d %ld %s %s] %s", tm.tm_year + 1900,
                           tm.tm_mon + 1,
                           tm.tm_mday,
                           tm.tm_hour,
                           tm.tm_min,
                           tm.tm_sec,
                           node->tv.tv_usec / 1000,
                           s_levelstr[node->level],
                           node->srcinfo,
                           node->logstr);
    fflush(fp);
    if (writelen > 0) {
      info->logsize += writelen;
      if (info->logsize > info->maxlogfilesize) {
        fclose(fp);
        // move file to file.bak
        char baklogfile[MAXPATH];
        sprintf(baklogfile, "%s.bak", info->logfile);

        struct stat buffer;
        if (stat(baklogfile, &buffer) == 0) {
          remove(baklogfile);
        }

        if (-1 == rename(info->logfile, baklogfile)) {
          perror("fail to rename!");
          exit(-1);
        } else {
          info->fp = fopen(info->logfile, "a");
          struct stat st;
          fstat(fileno(info->fp), &st);
          info->logsize = st.st_size;
        }
      }
    }
  }
  /*if (streamlevel <= node->level && streamlevel != PURECLOG_CLOSE) {
    char *color, *bkcolor, *style;
    getcolor(node->level, &color, &bkcolor, &style);
    printf("[%04d-%02d-%02d %02d:%02d:%02d %ld %s%s%s%s%s %s]:%s%s%s%s%s", tm.tm_year + 1900,
           tm.tm_mon + 1,
           tm.tm_mday,
           tm.tm_hour,
           tm.tm_min,
           tm.tm_sec,
           node->tv.tv_usec / 1000, color, bkcolor, style,
           s_levelstr[node->level], RESETCOLOR, node->srcinfo, color, bkcolor, style,
           node->logstr, RESETCOLOR);
  }*/
}
#ifdef _MSC_VER
unsigned int WINAPI logwritethread(LPVOID param) {
#else
void *logwritethread(void *param) {
#endif
  LOGFILEINFO *info = (LOGFILEINFO *)param;
  FILE *fp = fopen(info->logfile, "a");
  struct stat st;
  fstat(fileno(fp), &st);
  info->logsize = st.st_size;
  info->fp = fp;
  while (info->willrun) {
    LOGNODE *node = (LOGNODE *)SOLO_Read(info->logqueue);
    if (node) {
      outputstr(info, node); //->streamlevel,info->filelevel,fp,node);
      SOLO_Read_Over(info->logqueue);
    } else {
#ifdef _MSC_VER
      Sleep(5);
#else
      usleep(5 * 1000);
#endif
    }
  }

  LOGNODE *node = NULL;
  while ((node = (LOGNODE *)SOLO_Read(info->logqueue)) != NULL) {
    outputstr(info, node); //->streamlevel,info->filelevel,fp,node);
    SOLO_Read_Over(info->logqueue);
  }
  return 0;
}
static LOGFILEINFO *s_info = NULL;
void pureclog_init(LOGLEVEL streamlevel, LOGLEVEL filelevel, const char *logfile, int maxlogfilesize) {
#ifdef _MSC_VER
  DWORD mode;
  HANDLE consolehandle = GetStdHandle(STD_OUTPUT_HANDLE);
  if (GetConsoleMode(consolehandle, &mode)) {
    mode |= 0x0004; // ENABLE_VIRTUAL_TERMINAL_PROCESSING;
    if (!SetConsoleMode(consolehandle, mode)) {
      printf("fail to set console mode!\n");
    }
  } else {
    printf("fail to get console mode!\n");
  }
  InitializeCriticalSection(&s_section);
#endif
  s_info = (LOGFILEINFO *)calloc(1, sizeof(LOGFILEINFO));
  s_info->streamlevel = streamlevel;
  s_info->filelevel = filelevel;
  strcpy(s_info->logfile, logfile);
  s_info->maxlogfilesize = maxlogfilesize;
  s_info->logqueue = Create_Ring_Queue(1024, sizeof(LOGNODE));
  s_info->willrun = 1;
#ifdef _MSC_VER
  DWORD threadid;
  s_info->logthread = _beginthreadex(NULL, 0, logwritethread, s_info, 0, &threadid);
#else
  pthread_create(&s_info->logthread, NULL, logwritethread, s_info);
#endif
}
void pureclog_setstreamlevel(LOGLEVEL streamlevel) {
  s_info->streamlevel = streamlevel;
}
void pureclog_setfilelevel(LOGLEVEL filelevel) {
  s_info->filelevel = filelevel;
}
void pureclog_deinit() {
  if (s_info != NULL) {
    s_info->willrun = 0;
#ifdef _MSC_VER
    WaitForSingleObject(s_info->logthread, INFINITE);
    DeleteCriticalSection(&s_section);
#else
    pthread_join(s_info->logthread, NULL);
#endif
    Destroy_Ring_Queue(s_info->logqueue);
    free(s_info);
  }
}

#ifdef _MSC_VER
int gettimeofday(struct timeval *tp, void *tzp) {
  time_t clock;
  struct tm tm;
  SYSTEMTIME wtm;

  GetLocalTime(&wtm);
  tm.tm_year = wtm.wYear - 1900;
  tm.tm_mon = wtm.wMonth - 1;
  tm.tm_mday = wtm.wDay;
  tm.tm_hour = wtm.wHour;
  tm.tm_min = wtm.wMinute;
  tm.tm_sec = wtm.wSecond;
  tm.tm_isdst = -1;

  clock = mktime(&tm);
  tp->tv_sec = clock;
  tp->tv_usec = wtm.wMilliseconds * 1000;
  return (0);
}
#endif

static const char *mybasename(const char *file) {
  for (int i = strlen(file) - 1; i >= 0; i--) {
    if (file[i] == '/' || file[i] == '\\') {
      return (file + i + 1);
    }
  }
  return NULL;
}
// multithread call this function must get thread related queue
void pureclog_printf(const char *file, int line, const char *function, LOGLEVEL level, const char *fmt, ...) {
  if (s_info->streamlevel > level || s_info->streamlevel == PURECLOG_CLOSE) {
    if (s_info->filelevel > level || s_info->filelevel == PURECLOG_CLOSE) {
      return;
    }
  }
  va_list args;
  va_start(args, fmt);
#ifdef _MSC_VER
  EnterCriticalSection(&s_section);
#else
  pthread_mutex_lock(&s_mutex);
#endif
  LOGNODE *node = (LOGNODE *)SOLO_Write(s_info->logqueue);
  if (node) {
    gettimeofday(&node->tv, NULL);
    node->level = level;
    char *tmpfile = mybasename(file);
    if (tmpfile != NULL) {
      sprintf(node->srcinfo, "%s:%d:%s", mybasename(file), line, function);
    } else {
      sprintf(node->srcinfo, "%s:%d:%s", file, line, function);
    }
    vsnprintf(node->logstr, MAXLOGSIZE, fmt, args);
    SOLO_Write_Over(s_info->logqueue);
  } else {
    printf("fail to pureclog_printf!\n");
  }

  va_end(args);

  {
    va_list args;
    va_start(args, fmt);
    struct timeval tv;
    gettimeofday(&tv, NULL);

    char *tmpfile = mybasename(file);
    if (tmpfile != NULL) {
      sprintf(s_info->consolesrcinfo, "%s:%d:%s", mybasename(file), line, function);
    } else {
      sprintf(s_info->consolesrcinfo, "%s:%d:%s", file, line, function);
    }
    vsnprintf(s_info->consolelogstr, MAXLOGSIZE, fmt, args);
    outputstr_console(s_info, tv, level);

    va_end(args);
  }
#ifdef _MSC_VER
  LeaveCriticalSection(&s_section);
#else
  pthread_mutex_unlock(&s_mutex);
#endif
}
